package controller

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	appsv1 "k8s.io/api/apps/v1"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/log"

	v1alpha1 "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
	"github.com/aquasecurity/trivy-operator/pkg/kube"
	"github.com/aquasecurity/trivy-operator/pkg/operator/etc"
	"github.com/aquasecurity/trivy-operator/pkg/trivyoperator"
	"github.com/aquasecurity/trivy-operator/pkg/vulnerabilityreport"
)

// Function for creating all the required objects required for the tests to succeed
func setupTestEnvironment(testid string) (*ScanJobController, *batchv1.Job, error) {

	config, _ := etc.GetOperatorConfig()
	config.ExposedSecretScannerEnabled = false
	logger := log.Log.WithName("test-deletion-of-completed-scan-jobs")
	scannedResourceName := "scan-job-ttl-test-replicaset-" + testid
	namespace := "default"
	containerName := "scanner"

	// Create the replica set that was scanned
	replicaSet := &appsv1.ReplicaSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      scannedResourceName,
			Namespace: namespace,
		},
	}

	// Create the vulnerability report for the scanned replica set with the appropriate labels
	vulnReport := v1alpha1.VulnerabilityReport{
		TypeMeta: metav1.TypeMeta{
			APIVersion: v1alpha1.SchemeGroupVersion.String(),
			Kind:       "Job",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "scan-job-ttl-test-vuln-report" + testid,
			Namespace: namespace,
			Labels: map[string]string{
				trivyoperator.LabelResourceKind:      "ReplicaSet",
				trivyoperator.LabelResourceNamespace: namespace,
				trivyoperator.LabelResourceName:      scannedResourceName,
				trivyoperator.LabelContainerName:     containerName,
				trivyoperator.LabelResourceSpecHash:  "hash",
			},
		},
	}

	containerImages := kube.ContainerImages{
		"scanner": "trivy:latest",
	}
	containerImagesAsJob, _ := containerImages.AsJSON()

	// Create the job that scanned the replica set. Needs to be in state completed
	job := &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "scan-job",
			Namespace: namespace,
			Labels: map[string]string{
				trivyoperator.LabelResourceSpecHash:  "hash",
				trivyoperator.LabelResourceKind:      "ReplicaSet",
				trivyoperator.LabelResourceNamespace: namespace,
				trivyoperator.LabelResourceName:      scannedResourceName,
			},
			Annotations: map[string]string{
				trivyoperator.AnnotationContainerImages: containerImagesAsJob,
			},
		},
		Spec: batchv1.JobSpec{
			Template: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  containerName,
							Image: "trivy:latest",
						},
					},
					RestartPolicy: corev1.RestartPolicyNever,
				},
			},
		},
		Status: batchv1.JobStatus{
			Conditions: []batchv1.JobCondition{
				{
					Type: batchv1.JobComplete,
				},
			},
		},
	}

	client := fake.NewClientBuilder().WithScheme(trivyoperator.NewScheme()).WithObjects(job, replicaSet, &vulnReport).Build()
	objectResolver := kube.NewObjectResolver(client, nil)
	instance := &ScanJobController{
		Logger:                  logger,
		Config:                  config,
		VulnerabilityReadWriter: vulnerabilityreport.NewReadWriter(&objectResolver),
	}
	instance.Client = client

	return instance, job, nil
}

func TestDeletionOfCompletedScanJobs(t *testing.T) {
	tests := []struct {
		testId             string // Used to differentiate between tests
		name               string // Name of the test
		scannerJobTTL      int32  // TTL for the actual job that does the scanning
		wantScanJobDeleted bool   // true if the job should have been deleted after completion
	}{
		{
			testId:             "1",
			name:               "Job immediately deleted when ScannerReportTTL is empty",
			scannerJobTTL:      0,
			wantScanJobDeleted: true,
		},
		{
			testId:             "2",
			name:               "Job not deleted when ScannerReportTTL has been set",
			scannerJobTTL:      120,
			wantScanJobDeleted: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			instance, job, err := setupTestEnvironment(tt.testId)
			require.NoError(t, err)

			// Set the ttl for the cronjob itself if required by the test spec
			if tt.scannerJobTTL > 0 {
				job.Spec.TTLSecondsAfterFinished = &tt.scannerJobTTL
			}

			err = instance.processCompleteScanJob(t.Context(), job, "scanner")
			require.NoError(t, err)

			// Fetch the job
			jobAfterCompletion := &batchv1.Job{}
			err = instance.Client.Get(t.Context(), types.NamespacedName{Name: job.Name, Namespace: job.Namespace}, jobAfterCompletion)

			// If the test spec requires the job to be deleted, check that it has been indeed deleted
			if tt.wantScanJobDeleted {
				require.Error(t, err)
				require.True(t, apierrors.IsNotFound(err))
				return
			}
			require.NoError(t, err)
			require.NotNil(t, jobAfterCompletion)
		})
	}
}

// Test data structures for streamReportToFile tests
type TestVulnerabilityReport struct {
	APIVersion string                `json:"apiVersion"`
	Kind       string                `json:"kind"`
	Metadata   TestReportMetadata    `json:"metadata"`
	Report     TestVulnerabilityData `json:"report"`
}

type TestReportMetadata struct {
	Name      string            `json:"name"`
	Namespace string            `json:"namespace"`
	Labels    map[string]string `json:"labels,omitempty"`
}

type TestVulnerabilityData struct {
	Scanner         TestScanner         `json:"scanner"`
	Summary         TestSummary         `json:"summary"`
	Vulnerabilities []TestVulnerability `json:"vulnerabilities"`
}

type TestScanner struct {
	Name    string `json:"name"`
	Vendor  string `json:"vendor"`
	Version string `json:"version"`
}

type TestSummary struct {
	CriticalCount int `json:"criticalCount"`
	HighCount     int `json:"highCount"`
	MediumCount   int `json:"mediumCount"`
	LowCount      int `json:"lowCount"`
}

type TestVulnerability struct {
	VulnerabilityID string   `json:"vulnerabilityID"`
	Severity        string   `json:"severity"`
	Title           string   `json:"title"`
	Description     string   `json:"description"`
	FixedVersion    string   `json:"fixedVersion,omitempty"`
	Links           []string `json:"links,omitempty"`
}

func TestStreamReportToFile(t *testing.T) {
	tests := []struct {
		name           string
		report         any
		setupFile      func(t *testing.T) string
		validateResult func(t *testing.T, filePath string, originalReport any)
		expectError    bool
		errorContains  string
	}{
		{
			name: "successful streaming of vulnerability report",
			report: TestVulnerabilityReport{
				APIVersion: "aquasecurity.github.io/v1alpha1",
				Kind:       "VulnerabilityReport",
				Metadata: TestReportMetadata{
					Name:      "test-vulnerability-report",
					Namespace: "default",
					Labels: map[string]string{
						"app.kubernetes.io/name":  "test-app",
						"trivy-operator.resource": "test",
					},
				},
				Report: TestVulnerabilityData{
					Scanner: TestScanner{
						Name:    "Trivy",
						Vendor:  "Aqua Security",
						Version: "v0.67.0",
					},
					Summary: TestSummary{
						CriticalCount: 2,
						HighCount:     5,
						MediumCount:   3,
						LowCount:      1,
					},
					Vulnerabilities: []TestVulnerability{
						{
							VulnerabilityID: "CVE-2023-1234",
							Severity:        "CRITICAL",
							Title:           "Test Critical Vulnerability",
							Description:     "A test critical vulnerability for unit testing",
							FixedVersion:    "1.2.3",
							Links:           []string{"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-1234"},
						},
						{
							VulnerabilityID: "CVE-2023-5678",
							Severity:        "HIGH",
							Title:           "Test High Vulnerability",
							Description:     "A test high vulnerability for unit testing",
							FixedVersion:    "1.2.4",
							Links:           []string{"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-5678"},
						},
					},
				},
			},
			setupFile: func(t *testing.T) string {
				tmpDir := t.TempDir()
				return filepath.Join(tmpDir, "vulnerability-report.json")
			},
			validateResult: func(t *testing.T, filePath string, originalReport any) {
				// Check file exists
				assert.FileExists(t, filePath)

				// Read file content
				content, err := os.ReadFile(filePath)
				require.NoError(t, err)

				// Validate JSON structure
				var decodedReport TestVulnerabilityReport
				err = json.Unmarshal(content, &decodedReport)
				require.NoError(t, err)

				// Validate content matches original
				original := originalReport.(TestVulnerabilityReport)
				assert.Equal(t, original.APIVersion, decodedReport.APIVersion)
				assert.Equal(t, original.Kind, decodedReport.Kind)
				assert.Equal(t, original.Metadata.Name, decodedReport.Metadata.Name)
				assert.Equal(t, original.Report.Scanner.Name, decodedReport.Report.Scanner.Name)
				assert.Equal(t, original.Report.Summary.CriticalCount, decodedReport.Report.Summary.CriticalCount)
				assert.Len(t, decodedReport.Report.Vulnerabilities, 2)

				// Validate pretty printing (indentation)
				assert.Contains(t, string(content), "  \"apiVersion\":")
				assert.Contains(t, string(content), "    \"name\":")
			},
			expectError: false,
		},
		{
			name:   "failure due to invalid directory path",
			report: map[string]string{"test": "data"},
			setupFile: func(_ *testing.T) string {
				return "/nonexistent/directory/report.json"
			},
			validateResult: func(t *testing.T, filePath string, _ any) {
				// File should not exist
				assert.NoFileExists(t, filePath)
			},
			expectError:   true,
			errorContains: "failed to create file",
		},
		{
			name: "failure due to non-serializable data",
			report: map[string]any{
				"valid":   "data",
				"invalid": make(chan int), // Channels cannot be JSON marshaled
			},
			setupFile: func(t *testing.T) string {
				tmpDir := t.TempDir()
				return filepath.Join(tmpDir, "invalid-report.json")
			},
			validateResult: func(t *testing.T, filePath string, _ any) {
				// File may be created but content will be invalid
				info, err := os.Stat(filePath)
				if err == nil {
					// If file exists, it should be empty or contain invalid JSON
					assert.Equal(t, int64(0), info.Size())
				}
			},
			expectError:   true,
			errorContains: "failed to encode report",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			filePath := tt.setupFile(t)

			// Execute the function under test
			err := streamReportToFile(tt.report, filePath)

			// Validate error expectations
			if tt.expectError {
				require.Error(t, err)
				if tt.errorContains != "" {
					assert.Contains(t, err.Error(), tt.errorContains)
				}
			} else {
				require.NoError(t, err)
			}

			// Validate results
			tt.validateResult(t, filePath, tt.report)
		})
	}
}

func TestStreamReportToFilePermissions(t *testing.T) {
	tmpDir := t.TempDir()
	filePath := filepath.Join(tmpDir, "permissions-test.json")

	report := map[string]string{
		"test": "data for permissions test",
	}

	err := streamReportToFile(report, filePath)
	require.NoError(t, err)

	// Check file permissions
	info, err := os.Stat(filePath)
	require.NoError(t, err)

	// File should be readable and writable by owner
	mode := info.Mode()
	assert.True(t, mode.IsRegular())

	// Verify file can be read
	content, err := os.ReadFile(filePath)
	require.NoError(t, err)
	assert.Contains(t, string(content), "permissions test")
}

// Benchmark tests to measure memory efficiency of streaming approach
func BenchmarkStreamReportToFile(b *testing.B) {
	tmpDir := b.TempDir()

	// Create a moderately sized report
	vulnerabilities := make([]TestVulnerability, 100)
	for i := 0; i < 100; i++ {
		vulnerabilities[i] = TestVulnerability{
			VulnerabilityID: fmt.Sprintf("CVE-2023-%04d", i),
			Severity:        "MEDIUM",
			Title:           fmt.Sprintf("Benchmark Vulnerability %d", i),
			Description:     fmt.Sprintf("A benchmark vulnerability number %d for performance testing", i),
			FixedVersion:    "1.0.0",
			Links:           []string{fmt.Sprintf("https://example.com/cve-%d", i)},
		}
	}

	report := TestVulnerabilityReport{
		APIVersion: "aquasecurity.github.io/v1alpha1",
		Kind:       "VulnerabilityReport",
		Metadata: TestReportMetadata{
			Name:      "benchmark-vulnerability-report",
			Namespace: "default",
		},
		Report: TestVulnerabilityData{
			Scanner: TestScanner{
				Name:    "Trivy",
				Vendor:  "Aqua Security",
				Version: "v0.67.0",
			},
			Summary: TestSummary{
				MediumCount: 100,
			},
			Vulnerabilities: vulnerabilities,
		},
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		filePath := filepath.Join(tmpDir, fmt.Sprintf("benchmark-report-%d.json", i))
		err := streamReportToFile(report, filePath)
		if err != nil {
			b.Fatal(err)
		}
	}
}
